#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Level.h>
#include <Kharlia/Engine/TileData.h>
#include <Kharlia/Engine/Actor.h>
#include <Kharlia/Engine/Engine.h>
#include <Kharlia/Engine/World.h>
#include <Kharlia/Engine/Utils.h>
#include <Kharlia/Engine/Player.h>
#include <Kharlia/Engine/Net.h>
#include <Kharlia/Core/App.h>
#include <Kharlia/Core/ObjectUtil.h>
#include <Kharlia/Engine/Debug.h>

#define TILE_W 16
#define TILE_H 16
#define TILE_EX 2

#define TILE_W_F 16.0f
#define TILE_H_F 16.0f
#define TILE_EX_F 2.0f

#define TILE_VERTEX_COUNT 4
#define TILE_VERTEX_SIZE 2
#define TILE_TEX_COORD_COUNT 4
#define TILE_TEX_COORD_SIZE 2

namespace kharlia { namespace engine {

template <class T>
inline T Clamp(const T& value, const T& low, const T& high)
{
    return value < low ? low : (value > high ? high : value);
}

// -------------------------
// FTileLayer
// -------------------------

tuple PSFTileLayer::getinitargs(FTileLayer& tl) {
    return py::make_tuple(tl.GetIndex(), tl.GetTileData());
}

FTileLayer::FTileLayer(int index):
    FDrawable(),
    mIndex(index),
    mTileData()
{
}

FTileLayer::FTileLayer(int index, const FSizeI& size):
    FDrawable(),
    mIndex(index),
    mTileData(new FTileData(size))
{
}

FTileLayer::FTileLayer(int index, shared_ptr<FTileData> tile_data):
    FDrawable(),
    mIndex(index),
    mTileData(tile_data)
{
}

FTileLayer::~FTileLayer() {
    
}

// -------------------------
// FTileGroup
// -------------------------

tuple PSFTileGroup::getinitargs(FTileGroup& tg) {
    return make_tuple(tg.GetSize());
}

tuple PSFTileGroup::getstate(FTileGroup& tg) {
    list indices, layers;
    FTileGroup::LayerIterator it;

    for (it = tg.mLayers.begin(); it != tg.mLayers.end(); it++) {
        indices.append(it->first);
        layers.append(it->second);
    }

    return make_tuple(indices, layers);
}

void PSFTileGroup::setstate(FTileGroup& tg, tuple state) {
    list indices = extract<list>(state[0]),
                layers = extract<list>(state[1]);

    for (int i = 0; i < len(indices); i++) {
        tg.AddLayer(extract<int>(indices[i]),
                    extract<shared_ptr<FTileLayer>>(layers[i]));
    }
}

FTileGroup::FTileGroup(const FSizeI& size):
    mSize(size) {}

void FTileGroup::AddLayer(int index) {
    LayerIterator it = mLayers.find(index);
    if (it != mLayers.end())
        throw RuntimeError(format("layer with index '%1%' already exists") % index);
    shared_ptr<FTileLayer> tl = make_shared<FTileLayer>(index, mSize);
    mLayers[index] = tl;
}

void FTileGroup::AddLayer(int index, shared_ptr<FTileLayer> tl) {
    LayerIterator it = mLayers.find(index);
    if (it != mLayers.end()) {
        throw RuntimeError(format("layer with index '%1%' already exists") % index);
    }
    mLayers[index] = tl;
}

const shared_ptr<FTileLayer>& FTileGroup::GetLayer(int index) {
    LayerIterator it = mLayers.find(index);
    if (it == mLayers.end())
        throw Exception("layer does not exist");
    return it->second;
}

std::vector<int> FTileGroup::GetLayerIndices() {
    std::vector<int> out;
    LayerIterator it;
    for (it = mLayers.begin(); it != mLayers.end(); it++) {
        out.push_back(it->first);
    }
    return out;
}

list FTileGroup::GetLayerIndicesPy() {
    list out;
    LayerIterator it; 
    for (it = mLayers.begin(); it != mLayers.end(); it++) {
        out.append(it->first);
    }
    return out;
}

// -------------------------
// CLevel
// -------------------------

/// InitArgs (handled in reduce): Name, GUID, Size
/// State: LayerInfos, TileGroups, Actors, Collidables, Renderables
tuple PSCLevel::getstate(CLevel& level) {
    using namespace objects;

    list layerInfos, tileGroups, actors, collidables, renderables;

    CLevel::LayerInfosType::iterator lit;
    for (lit = level.mLayerInfos.begin(); lit != level.mLayerInfos.end(); lit++) {
        layerInfos.append(lit->second);
    }

    CLevel::TileGroupsType::iterator tit;
    for (tit = level.mTileGroups.begin(); tit != level.mTileGroups.end(); tit++) {
        tileGroups.append(*tit);
    }

    object actor;
    for (uint i = 0; i < level.mActors.size(); i++) {
        actor = level.mActors[i]->GetSelf();
        if (!actor.attr("Transient")) {
            actors.append(actor);
            //actors.append(py::make_tuple(
            //    level.mActors[i]->GetOwner(),
            //    level.mActors[i]->GetName(),
            //    actor
            //));
        }
    }

    for (uint i = 0; i < level.mCollidables.size(); i++) {
        collidables.append(level.mCollidables[i]);
    }

    for (uint i = 0; i < level.mRenderables.size(); i++) {
        renderables.append(level.mRenderables[i]);
    }

    object idict;
    {
        instance<>* self = reinterpret_cast<instance<>*>(level.mSelf);
        if (self->dict)
            idict = object(handle<>(borrowed(self->dict)));
    }

    return py::make_tuple(idict, layerInfos, tileGroups, actors,
                          renderables, collidables, level.mIDCounter);
}

/// Restore the level from state. Actors are created and added to actor
/// list automatically as they're unpickled, this should probably be undone.
void PSCLevel::setstate(CLevel& level, tuple state) {
    using namespace objects;

    assert(level.mLayerInfos.size() == 0 && level.mTileGroups.size() == 0);

    object idictO    = state[0];
    list layerInfos  = extract<list>(state[1]);
    list tileGroups  = extract<list>(state[2]);
    list actors      = extract<list>(state[3]);
    list renderables = extract<list>(state[4]);
    list collidables = extract<list>(state[5]);
    uint32 idcounter = extract<uint32>(state[6]);

    level.mIDCounter = idcounter;

    if (!idictO.is_none()) {
        dict idict = extract<dict>(idictO);
        instance<>* self = reinterpret_cast<instance<>*>(level.mSelf);
        if (self->dict)
            PyDict_Update(self->dict, idict.ptr());
        else
            self->dict = incref(idict.ptr());
    }

    object obj,
    nextfunc = core::GetIterNextFunc(layerInfos);
    while (core::Iterate(nextfunc, obj)) {
        shared_ptr<FLayerInfo> li = extract<shared_ptr<FLayerInfo>>(obj);
        level.mLayerInfos[li->Index] = li;
    }
    nextfunc = core::GetIterNextFunc(tileGroups);
    while (core::Iterate(nextfunc, obj)) {
        shared_ptr<FTileGroup> tg = extract<shared_ptr<FTileGroup>>(obj);
        level.mTileGroups.push_back(tg);
    }

    nextfunc = GetIterNextFunc(actors);
    while (Iterate(nextfunc, obj)) {
        shared_ptr<CActor> actor = extract<shared_ptr<CActor>>(obj);
        assert(actor->GetID() != 0);
        assert(actor->GetLevel().get() == &level);
        level.AddActor(actor);
        //shared_ptr<CActor> owner = extract<shared_ptr<CActor>>(obj[0]);
        //object name = obj[1];
        //shared_ptr<CActor> actor = extract<shared_ptr<CActor>>(obj[2]);
        //level.CreateActor(object(), owner, name, actor);
    }

    for (int i = 0; i < len(collidables); i++) {
        shared_ptr<APhysics> col = extract<shared_ptr<APhysics>>(collidables[i]);
        level.mCollidables.push_back(col);
    }

    for (int i = 0; i < len(renderables); i++) {
        shared_ptr<ARender> ren = extract<shared_ptr<ARender>>(renderables[i]);
        level.mRenderables.push_back(ren);
    }
}

object CLevel::msPlayerControllerClass;
object CLevel::msActorClass;

CLevel::CLevel(PyObject* self):
    CObject(self),
    mWorld(),
    mSize(),
    mTileGroupSize(FTileGroup::WIDTH, FTileGroup::HEIGHT),
    mTileGroups(),
    mName(),
    mGUID(),
    mActors(),
    mLayerInfos(),
    mTileset(),
    mActorTasker(),
    mTime(0.0),
    mTimeDilation(1.0),
    mIDCounter(0)
{
}

CLevel::~CLevel() {
}

void CLevel::Init(shared_ptr<CWorld> world, object guid, unicode name, const FSizeI& size) {
    if (mWorld)
        return;

    if (size.Width > MAX_WIDTH || size.Height > MAX_HEIGHT) {
        throw ValueError("level size limited to 255x255 tile groups");
    }

    mWorld = world;
    mLogger = world->GetLogger();
    mGUID = guid;
    mName = name;
    mSize = size;
    mNameHash = name.attr("__hash__")();
    assert(PyInt_CheckExact(mNameHash.ptr()));

    // Create our tasklet manager
    core::FTaskletGroupOptions ops;
    ops.InsertStraysIntoDefault = true;
    ops.RunPreference = 1;

    object core_ = import("kharlia.core");
    mActorTasker = ConstructObjectEx<core::CTasker>(core_.attr("CTasker"), GROUP_COUNT, ops);
}

void CLevel::Exit() {
    if (!mWorld)
        return;

    mActorTasker->KillTasklets(core::CTasker::ALL_GROUPS);
    
    ClearActors();
    assert(mRenderables.size() == 0);
    assert(mCollidables.size() == 0);
    
    mTileGroups.clear();
    mTileset.reset();

    mWorld.reset();
    mLogger.reset();
}

void CLevel::_OnCreated() {}
void CLevel::_OnDestroyed() {}

void CLevel::Test(int num) {
    // Level test code
    if (num == 0) {
        // Tileset testing code
        mTileset = make_shared<FTileset>("Default", "testtiles.png", FSizeI(16, 32));
        // Make the bush tiles blocking
        uint16 blocking[] = {74, 75, 90, 91};
        BOOST_FOREACH(uint16& i, blocking) {
            mTileset->TileInfo.at(i).Blocking = true;
        }
    }
}

void CLevel::ActorTickerMain() {
    assert(!core::IsCurrentTaskletMain());
    assert(mWorld);
    float delta = 0;

    while (mWorld) {
        TickActors(delta);
        delta = mActorTasker->Yield(TICK_GROUP);
    }
}

void CLevel::Tick(float deltaTime) {
    if (mWorld->GetGameStarted()) {
        mTime += deltaTime;
    }
    mActorTasker->Tick(deltaTime, core::CTasker::ALL_GROUPS);
}

void CLevel::TickActors(float deltaTime) {
    for (uint i = 0; i < mActors.size(); i++) {
        mActors[i]->Tick(deltaTime);
    }
}

uint32 CLevel::CreateID() {
    if (mIDCounter >= 0xFFFFFFFF)
        throw NotImplementedError("no more IDs");
    // Zero is invalid
    return ++mIDCounter;
}

namespace {

// This function takes the argument names and produces a list of component
// types from it. names can be None, a string (type name), a type, or an iterable object
// that yields either types or type names.
list GetComponentTypes(object names) {
    object base = GetModuleAttr("AComponentBase");
    assert(PyType_Check(base.ptr()));
    list types;
    if (names.is_none()) {
        // do nothing
    }
    // Check if names is a string, and resolve it
    else if (PyString_Check(names.ptr())) {
        object comptype = ResolveName(names);
        if (!PyType_Check(comptype.ptr()) ||
                !PyObject_IsSubclass(comptype.ptr(), base.ptr())) {
            throw TypeError("component name must resolve to subclass of AComponentBase");
        }
        types.append(comptype);
    }
    // Check if name is a type, and append it
    else if (PyType_Check(names.ptr()) &&
             PyObject_IsSubclass(names.ptr(), base.ptr())) {
        types.append(names);
    }
    // Attempt to iterate through the object
    else {
        py::handle<> iter(allow_null(PyObject_GetIter(names.ptr())));

        if (!iter) {
            PyErr_Clear();
            throw TypeError("comps must be None, string, or iterable object");
        }

        object compname, next = GetIterNextFunc(object(iter));
        while (Iterate(next, compname)) {
            // If its a string, resolve it to get the type
            if (PyString_Check(compname.ptr())) {
                object comptype = ResolveName(compname);
                if (!PyType_Check(comptype.ptr()) ||
                        !PyObject_IsSubclass(comptype.ptr(), base.ptr())) {
                    throw TypeError("component name must resolve to subclass of AComponentBase");
                }
                types.append(comptype);
            }
            // If its a type, ensure its a component type
            else if (PyType_Check(compname.ptr()) &&
                     PyObject_IsSubclass(compname.ptr(), base.ptr())) {
                types.append(compname);
            }
            // Otherwise, something is wrong
            else {
                str os(compname);
                format msg("invalid component specification: %1%");
                msg % PyString_AS_STRING(os.ptr());
                throw TypeError(msg);
            }
        }
    }
    return types;
}

} // namespace

void CLevel::AddActor(shared_ptr<CActor> actor) {
    assert(actor);
    mActors.push_back(actor);
    mActorIDMap[actor->GetID()] = actor->GetSelf();
}

void CLevel::RemoveActor(shared_ptr<CActor> actor, uint32 id) {
    assert(actor);
    if (id == 0)
        id = actor->GetID();
    mActors.erase(std::find(mActors.begin(), mActors.end(), actor));
    mActorIDMap[id].del();
}

object CLevel::CreateActor(
    object compnames,
    shared_ptr<CActor> owner,
    shared_ptr<CActor> obj)
{
    if (msActorClass.is_none())
        throw RuntimeError("actor class unspecified");
    if (obj && obj->GetLevel())
        throw ValueError("invalid obj, already in use");

    list comptypes = GetComponentTypes(compnames);
    
    // Create ID
    uint32 id = CreateID();
    assert(id > 0);

    // Check for ID collision
    if (mActorIDMap.contains(id)) {
        throw RuntimeError(format("id %1% already in use; bad state") % id);
    }
    // Construct and initialize
    object actorO;
    shared_ptr<CActor> actor;
    // Use existing object
    if (obj) {
        actor = obj;
        actorO = obj->GetSelf();
    }
    // Create new object
    else {
        actorO = msActorClass();
        actor = extract<shared_ptr<CActor>>(actorO);
    }
    
    assert(actor->GetID() == 0);
    actor->OnInit(id, GetSelfPtr<CLevel>(), owner);
    assert(actor->GetID() == id);
    // Add to level's list and dict
    AddActor(actor);

    // Add components
    if (len(comptypes) > 0) {
        try {
            for (int i = 0; i < len(comptypes); i++) {
                object compio = comptypes[i]();
                shared_ptr<AComponent> compi = extract<shared_ptr<AComponent>>(compio);
                actor->AddComponent(compi);
            }
        }
        catch (...) {
            actor->ClearComponents();
            throw;
        }
    }

    // Call creation event
    actor->OnCreated();

    if (mWorld->GetGameStarted())
        actor->OnGameStarted(mWorld->GetGame());

    return actorO;
}

void CLevel::DestroyActor(shared_ptr<CActor> actor) {
    object actoro(actor);

    // This level can only destroy its own actors
    if (actor->GetLevel().get() != this) {
        throw RuntimeError("actor is not in this level");
    }

    if (mWorld->GetGameStarted())
        actor->OnGameStopped();

    actor->OnDestroyed();

    uint32 id = actor->GetID();
    assert(id != 0);

    actor->OnExit();
    assert(actor->GetID() == 0);

    RemoveActor(actor, id);
}

shared_ptr<CActor> CLevel::GetActorByTag(object tag) const {
    extract<unicode> extractor(tag);
    if (!extractor.check())
        throw TypeError("not a unicode string");
    for (uint i = 0; i < mActors.size(); i++) {
        if (mActors[i]->GetTag() == tag) {
            return mActors[i];
        }
    }
    return shared_ptr<CActor>();
}

shared_ptr<CActor> CLevel::GetActorByID(uint32 id) const {
    object act = mActorIDMap.get(id, object());
    if (act.is_none())
        throw ValueError(format("actor not found with id %1%") % id);
    return extract<shared_ptr<CActor>>(act);
}

shared_ptr<APlayerController> CLevel::CreatePlayerActor(
    shared_ptr<CPlayer> player, ENetRole role
) {
    if (msPlayerControllerClass.is_none())
        throw RuntimeError("player controller class undefined");
    
    // GetGame()->Login(
    shared_ptr<CActor> pcactor = extract<shared_ptr<CActor>>(CreateActor(msPlayerControllerClass));
    shared_ptr<APlayerController> pc = pcactor->GetPlayerController();

    pc->SetPlayer(player);
    player->SetController(pc);

    //pc->SetNetRole(ROLE_AUTHORITY);
    //pc->SetNetRemoteRole(role);

    return pc;
}

shared_ptr<FLayerInfo> CLevel::CreateLayer(int index) {
    if (GetLayerInfo(index))
        throw RuntimeError("layer with that index already exists");
    if (mTileGroups.size() < 1) {
        for (int i = 0; i < mSize.Height * mSize.Width; i++) {
            shared_ptr<FTileGroup> tileGroup = make_shared<FTileGroup>(mTileGroupSize);
            mTileGroups.push_back(tileGroup);
        }
    }
    for (uint i = 0; i < mTileGroups.size(); i++) {
        mTileGroups[i]->AddLayer(index);
    }
    shared_ptr<FLayerInfo> info = make_shared<FLayerInfo>();
    info->Index = index;
    mLayerInfos[index] = info;
    return info;
}

void CLevel::OnGameStarted(shared_ptr<CGame> game) {
    mTime = 0.0;
    for (uint i = 0; i < mActors.size(); i++) {
        mActors[i]->OnGameStarted(game);
    }
}

void CLevel::OnGameStopped() {
    mTime = 0.0;
    for (uint i = 0; i < mActors.size(); i++) {
        mActors[i]->OnGameStopped();
    }
}

void CLevel::SetTiles(int layer, tile_t tile) {
    std::vector<shared_ptr<FTileGroup>>::iterator it;
    for (it = mTileGroups.begin(); it != mTileGroups.end(); it++) {
        (*it)->GetLayer(layer)->GetTileData()->SetTiles(tile);
    }
}

void CLevel::SetTiles(int layer, const FVector2I& topos, const FSizeI& size, const tile_t* tiles) {
    //const tile_t* front = &tiledata->GetTiles().front();
    shared_ptr<FTileData> td = make_shared<FTileData>(size);
    td->SetTiles(topos, size, tiles);
    //SetTiles(layer, topos, tiledata->GetSize(), front);
}


void CLevel::SetTiles(int layerIndex, const FVector2I& toPos, shared_ptr<FTileData> tileData) {
    const int& tileDataWidth = tileData->GetSize().Width;
    const int& tileDataHeight = tileData->GetSize().Height;

    // Top left and bottom right tile group positions
    FVector2I topLeftPos(toPos.X / mTileGroupSize.Width,
                         toPos.Y / mTileGroupSize.Height);
    FVector2I bottomRightPos((toPos.X + (tileDataWidth - 1)) / mTileGroupSize.Width,
                             (toPos.Y + (tileDataHeight - 1)) / mTileGroupSize.Height);
    
    // tile group width, height, and count
    int destTileGroupsWidth = bottomRightPos.X - topLeftPos.X + 1;
    int destTileGroupsHeight = bottomRightPos.Y - topLeftPos.Y + 1;
    int destTileGroupsCount = destTileGroupsWidth * destTileGroupsHeight;

    // Piece width, height; current tilegroup x, y; total offset x, y; tds index
    int pieceWidth, pieceHeight, groupX, groupY, offsetX, offsetY;
    shared_ptr<FTileLayer> layer;

    // get our position within the tile group
    // start in top left corner
    groupX = toPos.X % mTileGroupSize.Width;
    groupY = toPos.Y % mTileGroupSize.Height;
    offsetX = 0;
    offsetY = 0;
    for (int Y = 0; Y < destTileGroupsHeight; Y++) {
        // Calculate how many tiles from the brush can we fit in this group
        pieceHeight = mTileGroupSize.Height - groupY;
        if (pieceHeight > (tileDataHeight - offsetY))
            pieceHeight = tileDataHeight - offsetY;
        for (int X = 0; X < destTileGroupsWidth; X++) {
            pieceWidth = mTileGroupSize.Width - groupX;
            if (pieceWidth > (tileDataWidth - offsetX))
                pieceWidth = tileDataWidth - offsetX;
            
            // Destination tile group location on the level
            FVector2I destTileGroupPos(topLeftPos.X + X, topLeftPos.Y + Y);
            if (destTileGroupPos.X < 0 || destTileGroupPos.X > mSize.Width ||
                destTileGroupPos.Y < 0 || destTileGroupPos.Y > mSize.Height)
                continue;

            layer = this->GetTileGroup(destTileGroupPos)->GetLayer(layerIndex);
            layer->GetTileData()->CopyTiles(FVector2I(groupX, groupY),
                                            FRectI(offsetX, offsetY, pieceWidth, pieceHeight),
                                            tileData);
            // Must signal that tiles have been changed to recreate
            // texture coordinate buffers
            layer->SetModified(true);

            offsetX += pieceWidth;
            groupX = (toPos.X + offsetX) % mTileGroupSize.Width;
        }
        offsetY += pieceHeight;
        groupY = (toPos.Y + offsetY) % mTileGroupSize.Height;
    }
}
void CLevel::SetTiles(int layer, const FVector2I& topos, const FSizeI& size, tile_t tile) {
    // top left position of tile group on the level
    FVector2I tlp(topos.X / mTileGroupSize.Width,
                 topos.Y / mTileGroupSize.Height);

    // bottom right position of tile group on the level
    FVector2I brp((topos.X + size.Width) / mTileGroupSize.Width,
                 (topos.Y + size.Height) / mTileGroupSize.Height);

    // width and height of affected tile groups
    int tgw = brp.X - tlp.X + 1;
    int tgh = brp.Y - tlp.Y + 1;

    for (int h = 0; h < tgh; h++) {
        for (int w = 0; w < tgw; w++) {
            FVector2I tgp(tlp.X + w, tlp.Y + h);
            if (tgp.X < 0 || tgp.X >= mSize.Width ||
                tgp.Y < 0 || tgp.Y >= mSize.Height) {
                    continue;
            }
            // adjust from tile coordinates to pos on tiledata
            FVector2I adjpos(topos.X - (mTileGroupSize.Width * w),
                            topos.Y - (mTileGroupSize.Height * h));
            GetTileGroup(tgp)->
                GetLayer(layer)->
                GetTileData()->
                SetTiles(adjpos, size, tile);
        }
    }
}

void CLevel::AddCollidable(shared_ptr<APhysics> phys) {
    if (phys->GetLevel().get() != this) {
        throw RuntimeError("component is not owned by this level");
    }
    if (std::find(mCollidables.begin(), mCollidables.end(), phys) != mCollidables.end())
        return;
    mCollidables.push_back(phys);
}

void CLevel::RemoveCollidable(shared_ptr<APhysics> phys) {
    if (phys->GetLevel().get() != this) {
        throw RuntimeError("component is not owned by this level");
    }
    std::vector<shared_ptr<APhysics>>::iterator it =
        std::find(mCollidables.begin(), mCollidables.end(), phys);
    if (it != mCollidables.end())
        mCollidables.erase(it);
}

bool CLevel::AddRenderable(shared_ptr<ARender> r) {
    if (r->GetOwner()->GetLevel().get() != this)
        return false;
    if (std::find(mRenderables.begin(), mRenderables.end(), r) != mRenderables.end())
        return true;
    mRenderables.push_back(r);
    return true;
}

bool CLevel::RemoveRenderable(shared_ptr<ARender> r) {
    RenderablesType::iterator it = std::find(mRenderables.begin(), mRenderables.end(), r);
    if (it == mRenderables.end())
        return false;
    mRenderables.erase(it);
    return true;
}

bool CLevel::CollideTiles(const FRectF& dest, int layer) {
    FVector2I tilePos;
    const FTileInfo* tileInfo = NULL;

    // Check all tiles within the destination rect
    int w = int(ceil(dest.GetRight()) - floor(dest.GetLeft()));
    int h = int(ceil(dest.GetBottom()) - floor(dest.GetTop()));
    for (int hi = 0; hi < h; hi++) {
        for (int wi = 0; wi < w; wi++) {
            try {
                tilePos.X = int(dest.Pos.X) + wi;
                tilePos.Y = int(dest.Pos.Y) + hi;
                // TODO: Optimize locating the tile layer rather than
                //       looking through the layer map each time.
                tileInfo = GetTileInfo(layer, tilePos);
                assert(tileInfo);
                if (tileInfo->Blocking) {
                    return true;
                }
            }
            catch (std::out_of_range&) {
                mLogger->Warning("collision tile position out of range");
            }
        }
    }
    return false;
}

void CLevel::MoveActor(
    const shared_ptr<CActor>& actor,
    const FVector3F& delta,
    bool smooth,
    bool collideTiles,
    bool collideActors
) {
    const double TOUCH_TIME_LIMIT = 1.0;

    assert(actor->GetLevel().get() == this);

    const shared_ptr<ATransform>& trans = actor->GetTransform();
    if (!trans)
        return;

    const shared_ptr<APhysics>& phys = actor->GetPhysics();
    if (!phys) {
        trans->AddPosition(delta);
        return;
    }

    const FVector3F& pos = trans->GetPosition();
    const FRectF& bounds = phys->GetBounds();
    FVector3F dest       = pos + delta;
    int collLayer        = phys->GetCollisionLayer();
    bool hitTile         = false,
         hitActor        = false,
         hitEdge         = false,
         validBounds     = bounds.Size.Width > 0.0f && bounds.Size.Height > 0.0f;

    shared_ptr<APhysics> hitPhysTarget;

    float deltaLen = delta.GetLength();


    FRectF destRect = bounds;
    destRect.Pos.X += dest.X;
    destRect.Pos.Y += dest.Y;
    FVector2F nearTiles;

    // Check if we're in the level bounds
    if (destRect.GetLeft() < 0.0f ||
        destRect.GetTop() < 0.0f ||
        destRect.GetRight() > (mSize.Width * FTileGroup::WIDTH) ||
        destRect.GetBottom() > (mSize.Height * FTileGroup::HEIGHT))
    {
        hitEdge = true;
    }

    // Check tile collisions
    if (collideTiles && phys->GetCollideTiles() &&
            validBounds && !hitEdge && mTileGroups.size() > 0 && mTileset) {
        hitTile = CollideTiles(destRect, collLayer);
    }
    // Check actor collisions
    if (collideActors && phys->GetCollideActors() && validBounds && !hitEdge) {
        FTileGroupNumber destTGN = ATransform::CalculateTileGroupNumber(dest);
        for (uint i = 0; i < mCollidables.size(); i++) {
            const shared_ptr<APhysics>& targetPhysc = mCollidables[i];
            const shared_ptr<ATransform>& targetTrans = targetPhysc->GetTransform();

            // Simple checks
            if (targetTrans->GetTileGroupNumber() != destTGN ||
                !targetPhysc->GetCollideActors() ||
                targetPhysc->GetCollisionLayer() != collLayer ||
                targetPhysc->GetOwner() == actor)
                continue;

            // Check for valid bounds
            const FRectF& targetBounds = targetPhysc->GetBounds();
            if (targetBounds.Size.Width <= 0.0f || targetBounds.Size.Height <= 0.0f)
                continue;

            // Get target rect
            FRectF targetRect = targetBounds;
            targetRect.Pos.X += targetTrans->GetPosition().X;
            targetRect.Pos.Y += targetTrans->GetPosition().Y;

            // Bounding box check
            if ((targetRect.GetRight() < destRect.GetLeft()) ||
                (targetRect.GetLeft() > destRect.GetRight()) ||
                (targetRect.GetBottom() < destRect.GetTop()) ||
                (targetRect.GetTop() > destRect.GetBottom())) {
                continue;
            }
            else {
                hitActor = true;
                hitPhysTarget = targetPhysc;
                break;
            }
        }
    }
    if (hitTile || hitActor || hitEdge) {
        // Eventually implement event calls for responding to collisions
        if (hitActor && (phys->mLastActorHit != hitPhysTarget->GetOwner().get() ||
                         phys->mLastActorHitTime <= (mTime - TOUCH_TIME_LIMIT))) {
            object func, t;
            // Record raw pointer purely for comparing with another collision
            phys->mLastActorHit = hitPhysTarget->GetOwner().get();
            phys->mLastActorHitTime = mTime;

            //phys->CreateEventTasklet("OnTouched", POST_TICK_GROUP, py::make_tuple(hitPhysTarget));
            //hitPhysTarget->CreateEventTasklet("OnTouched", POST_TICK_GROUP, py::make_tuple(phys));
            
            // Call the event directly for now until I can properly test
            // how taskleting it out affects synchronization.
            //phys->OnTouched(hitPhysTarget);
            //hitPhysTarget->OnTouched(phys);

            // New event handling using an event processing tasklet
            //phys->SendMsg(str("Touched"), py::make_tuple(hitPhysTarget));
            //hitPhysTarget->SendMsg(str("Touched"), py::make_tuple(phys));
            phys->SendTouched(hitPhysTarget);
            hitPhysTarget->SendTouched(phys);
        }
    }
    else {
        trans->SetPosition(dest);
    }
}

void CLevel::SetPlayerControllerClass(object cls) {
    object pccls = GetTypeObject(type_id<APlayerController>());
    assert(!pccls.is_none());
    if (!PyObject_IsSubclass(cls.ptr(), pccls.ptr()))
        throw TypeError("player controller class must be subclass of APlayerControllerBase");
    msPlayerControllerClass = cls;
}

void CLevel::SetActorClass(object cls) {
    if (!cls.is_none()) {
        object actorBaseClass = GetTypeObject(type_id<CActor>());
        if (!PyObject_IsSubclass(cls.ptr(), actorBaseClass.ptr()))
            throw TypeError("actor class must be subclass of CActorBase");
    }
    msActorClass = cls;
}

object PSFLayerInfo::getstate(FLayerInfo& li) {
    rn::BitStream out;
    out.Write(li.Index);
    out.Write(li.Red);
    out.Write(li.Green);
    out.Write(li.Blue);
    out.Write(li.Alpha);
    out.Write(li.NoCollision);
    out.Write(li.Visible);
    return str((const char*)out.GetData(), out.GetNumberOfBytesUsed());
}

void PSFLayerInfo::setstate(FLayerInfo& li, str state) {
    rn::BitStream in((unsigned char*)PyString_AS_STRING(state.ptr()),
                        PyString_GET_SIZE(state.ptr()), false);
    in.Read(li.Index);
    in.Read(li.Red);
    in.Read(li.Green);
    in.Read(li.Blue);
    in.Read(li.Alpha);
    in.Read(li.NoCollision);
    in.Read(li.Visible);
}

KH_BEGIN_GC(CLevel)
    KH_GC_SHARED_PTR(thisx->mWorld)
    BOOST_FOREACH(shared_ptr<CActor>& a, thisx->mActors) {
        KH_GC_SHARED_PTR(a);
    }
KH_END_GC()

void _InitLevelClasses() {
    _InitLevelBindings();
}

void _ExitLevelClasses() {
    CLevel::msPlayerControllerClass = object();
    CLevel::msActorClass = object();
}

}} //namespace Kharlia
